/**
 * Bugs:
 *  - Double clicking too fast causes some markers to get lost in the ether
 *  - ClusterMarker index is sometimes 0; seen in IE; not yet reproduced in Firefox; click around a lot
 *  - zoom in several levels, pan several times; marker clusters are still at previous zoom level and not busted up
 *  - possible memory leaks; seems to slow down a bit in Firefox after continued use; not verified
 *
 * HiveClusterer based on HiveMapsClusterer.
 *
 * Note that the initial cluster pattern will be different with HiveClusterer than HiveMapsClusterer because
 * HiveMapsClusterer used an incrementing for loop to initially add the markers while HiveClusterer uses a 
 * drecrementing for loop. 
 *
 * @name HiveClusterer
 * @version 0.9
 * @author Joe Johnston
 * @copyright (c) 2009 Joe Johnston
 */

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


/**
 * @name HiveMapsClustererOptions
 * @class This class represents optional arguments to the {@link HiveMapsClusterer}
 * constructor.
 * @property {Number} [maxZoom] The max zoom level monitored by a
 * marker cluster. If not given, the marker cluster assumes the maximum map
 * zoom level. When maxZoom is reached or exceeded all markers will be shown
 * without cluster.
 * @property {Number} [gridSize=60] The grid size of a cluster in pixel. Each
 * cluster will be a square. If you want the algorithm to run faster, you can set
 * this value larger.
 * @property {Array of MarkerStyleOptions} [styles]
 * Custom styles for the cluster markers.
 * The array should be ordered according to increasing cluster size,
 * with the style for the smallest clusters first, and the style for the
 * largest clusters last.
 */

/**
 * @name MarkerStyleOptions
 * @class An array of these is passed into the {@link HiveMapsClustererOptions}
 * styles option.
 * @property {String} [url] Image url.
 * @property {Number} [height] Image height.
 * @property {Number} [height] Image width.
 * @property {Array of Number} [opt_anchor] Anchor for label text, like [24, 12]. 
 *    If not set, the text will align center and middle.
 * @property {String} [opt_textColor="black"] Text color.
 */

/**
 * Creates a new HiveMapsClusterer to cluster markers on the map.
 *
 * @constructor
 * @param {GMap2} map The map that the markers should be added to.
 * @param {Array of GMarker} opt_markers Initial set of markers to be clustered.
 * @param {HiveMapsClustererOptions} opt_opts A container for optional arguments.
 */
 
function HiveMapsClusterer(map, opt_markers, opt_opts) {
  // private members
  var clusters_ = [];
  var map_ = map;
  var maxZoom_ = null;
  var me_ = this;
  var gridSize_ = 60;
  var sizes = [53, 56, 66, 78, 90];
  var styles_ = [];
  var leftMarkers_ = [];
  var mcfn_ = null;
  var timers_ = {};
  var clusterCache_ = [];
  
  this.onDoneDrawing = null;
  this.debug = false;
    
  for (var i=5; i--;) {
    styles_.push({
      'url': "http://hivemaps.googlecode.com/svn/trunk/images/marker" + (i+1) + ".png",
      'height': sizes[i],
      'width': sizes[i]
    });
  }

  if (typeof opt_opts === "object" && opt_opts !== null) {
    if (typeof opt_opts.gridSize === "number" && opt_opts.gridSize > 0) {
      gridSize_ = opt_opts.gridSize;
    }
    if (typeof opt_opts.maxZoom === "number") {
      maxZoom_ = opt_opts.maxZoom;
    }
    if (typeof opt_opts.styles === "object" && opt_opts.styles !== null && opt_opts.styles.length !== 0) {
      styles_ = opt_opts.styles;
    }
  }

  function updateStats() {
    $('#debug')[0].innerHTML = '<div>Markers: '+me_.getTotalMarkers()+'</div><div>Cluster Cache: '+clusterCache_.length+'</div>'+
      '<div>Clusters: '+clusters_.length+'</div><div>Clusters in View: '+me_.getClustersInView().length+'</div>'+
      '<div>Clusters in View (forced): '+me_.getClustersInView(true).length+'</div><div>Free Markers: '+leftMarkers_.length+'</div>';
  }

  /**
   * Get cluster marker images of this marker cluster. Mostly used by {@link Cluster}
   * @private
   * @return {Array of String}
   */
  this.getStyles_ = function () {
    return styles_;
  };

  /**
   * Remove all markers.
   */
  this.clearMarkers = function () {
    for (var i=clusters_.length; i--;) {
      if (typeof clusters_[i] !== "undefined" && clusters_[i] !== null) {
        clusters_[i].clearMarkers();
      }
    }
    clusters_ = [];
    leftMarkers_ = [];
    GEvent.removeListener(mcfn_);
  };

  /**
  * Pre-populate clusters with calculations to save time in addMarker loops.
  * Always call this method before calling addMarker.  Clusters mapDivCenter properties
  * will be updated if the zoom level has changed.
  **/
  function prepClusters(clusters) {
    if (!clusters.length)
      return;
    var zoom = map_.getZoom();
    for (var i=clusters.length; i--;) {
      var cluster = clusters[i];
      if (cluster.zoom == zoom)
        continue;
      cluster.zoom = map_.getZoom();
      cluster.mapDivCenter = map_.fromLatLngToDivPixel(cluster.center);
      cluster.boundsLeft = cluster.mapDivCenter.x - gridSize_;
      cluster.boundsRight = cluster.mapDivCenter.x + gridSize_;
      cluster.boundsTop = cluster.mapDivCenter.y + gridSize_;
      cluster.boundsBottom = cluster.mapDivCenter.y - gridSize_;
    }
  }
  this.prepClusters = prepClusters;
  
  /**
  * Add free markers back into clusters.
  * @param {Array of GMarker} markers Markers to add.
  */
  function reAddMarkers_(markers) {
    // add leftMarkers to list of markers to process
    markers = markers.concat(leftMarkers_);
    // get all markers in view
    var inViewMarkers = [];
    leftMarkers_ = [];
    var len = markers.length;
    var bounds = map_.getBounds();
    for (var i=0; i<len; i++) {
      var marker = markers[i];
      if (bounds.containsLatLng(marker.latlng)) {
        inViewMarkers.push(marker);
      } else {
        leftMarkers_.push(marker);
      }
    }
    // create new clusters for markers in view
    var clusters = [];
    _addMarkers(inViewMarkers, clusters, function(markers, clusters){
      // cache the clusters in view
      clusterCache_ = clusters;
      // add new clusters to main list of clusters
      clusters_ = clusters_.concat(clusters);
      me_.redraw_();
      if (me_.debug) updateStats();
    });
  }
  
  function _addMarkers(markers, clusters, onDoneFunc, batchSize) {
    var len = markers.length;
    for (var i=0; i<len; i++)
      me_.addMarker(markers[i], clusters);
    if (onDoneFunc)
      onDoneFunc(markers, clusters);
    return;
    
		/*if (timers_.addMarkers) {
			clearTimeout(timers_.addMarkers);
			delete timers_.addMarkers;
			setTimeout(function(){_addMarkers(markers, clusters, opt_isNoCheck, onDoneFunc, batchSize);}, 200);
			return;
		}
    if (!batchSize) 
      batchSize = 200;
    timers_.addMarkers = setTimeout(function(){__addMarkers__(markers, clusters, onDoneFunc, batchSize, markers.length);}, 0);
    */
  }
  
  /*
  function __addMarkers__(markers, clusters, onDoneFunc, batchSize, cnt) {
		var n = (cnt-batchSize<0) ? cnt : batchSize;
		if (n > 0) {
			do {
        --cnt;
				if (!timers_.addMarkers) {
				  leftMarkers_ = markers.splice(0, cnt);
				  if (clusters != clusters_)
            clusters_ = clusters_.concat(clusters);
				  return;
				}
        me_.addMarker(markers[cnt], clusters);
    	} while(--n);
    }
		if (cnt) {
			timers_.addMarkers = setTimeout(function(){__addMarkers__(markers, clusters, onDoneFunc, batchSize, cnt);}, 0);
		} else {
			delete timers_.addMarkers;
			if (onDoneFunc)
			  onDoneFunc();
		}		
  }
  */
  
  /**
   * Add a marker.
   * @private
   * @param {GMarker} marker Marker you want to add
   * @param {Array of Cluster} opt_clusters Provide a list of clusters, the marker
   *     cluster will only check these cluster where the marker should join.
   */
  this.addMarker = function (marker, clusters) {
    // try to find a cluster that can contain the marker
    var pos = map_.fromLatLngToDivPixel(marker.getLatLng());
    var len = clusters.length;
    for (var i=0; i<len; i++) {
      var cluster = clusters[i];
      if (pos.x >= cluster.boundsLeft && pos.x <= cluster.boundsRight &&
          pos.y >= cluster.boundsBottom && pos.y <= cluster.boundsTop) {
        cluster.addMarker(marker);
        return cluster;
      }
    }
    // no existing cluster can contain the marker, so create a new cluster
    var cluster = new Cluster(this, marker.getLatLng());
    cluster.addMarker(marker);
    clusters.push(cluster);
    return cluster;
  };

  /**
  * Remove a marker.
  *
  * @param {GMarker} marker The marker you want to remove.
  */
  this.removeMarker = function (marker) {
    for (var i=clusters_.length; i--;) {
      if (clusters_[i].remove(marker)) {
        clusters_[i].redraw_();
        return;
      }
    }
  };

  /**
  * Redraw all clusters in viewport.
  */
  this.redraw_ = function () {
    if (timers_.redraw) {
      clearTimeout(timers_.redraw);
			delete timers_.redraw;
			setTimeout(function(){me_.redraw_();}, 400);
			return;
    }
    var clusters = me_.getClustersInView();
		timers_.redraw = setTimeout(function(){_redraw(clusters, clusters.length);}, 50);
  };
  
  function _redraw(clusters, cnt) {
    if (!timers_.redraw)
      return;
    if (!cnt) {
      delete timers_.redraw;
      if (me_.onDoneDrawing)
        me_.onDoneDrawing();
      return;
    }
    clusters[--cnt].redraw_(true);
		timers_.redraw = setTimeout(function(){_redraw(clusters, cnt);}, 0);
	}

  /**
  * Get all clusters in viewport.
  * @return {Array of Cluster}
  */
  this.getClustersInView = function (force) {
    if (!force && clusterCache_.length)
      return clusterCache_;
    var clusters = [];
    var len = clusters_.length;
    for (var i=0; i<len; i++) {
      if (clusters_[i].isInBounds())
        clusters.push(clusters_[i]);
    }
    clusterCache_ = clusters;
    return clusters;
  };

  /**
   * Get max zoom level.
   * @private
   * @return {Number}
   */
  this.getMaxZoom_ = function () {
    return maxZoom_;
  };

  /**
   * Get map object.
   * @private
   * @return {GMap2}
   */
  this.getMap_ = function () {
    return map_;
  };

  /**
   * Get grid size
   * @private
   * @return {Number}
   */
  this.getGridSize_ = function () {
    return gridSize_;
  };

  /**
   * Get total number of markers.
   * @return {Number}
   */
  this.getTotalMarkers = function () {
    var result = leftMarkers_.length;
    for (var i=clusters_.length; i--;)
      result += clusters_[i].getTotalMarkers();
    return result;
  };

  /**
   * Get total number of clusters.
   * @return {int}
   */
  this.getTotalClusters = function () {
    return clusters_.length;
  };
  
  this.getClusters = function() {
    return clusters_;
  };

  /**
   * Collect all markers of clusters in viewport and regroup them.
   */
  this.resetViewport = function () {        		
    clusterCache_ = [];
    var clusters = this.getClustersInView();
    var tmpMarkers = [];
    var removed = [];
    var mapZoom = map_.getZoom();
    var len = clusters.length;
    // increment through each loop to keep markers in order; do not optimize
    for (var i=0; i<len; i++) {
      var cluster = clusters[i];
      // protect against rare case where chunked addMarkers loop creates empty clusters
      if (cluster.markers.length == 0) {
        cluster.clearMarkers(true);
        removed.push(cluster);        
      // if zoom changed, extract the markers back onto the heap and destroy the cluster
      } else if (cluster.zoom !== mapZoom) {
        tmpMarkers = tmpMarkers.concat(cluster.markers);
        cluster.clearMarkers(true);
        removed.push(cluster);
      }
    }
    // delete the removed clusters from the cluster list
    var a=[], len=clusters_.length;
    for (var i=0,j=0; i<len; i++) {
      (clusters_[i]!=removed[j]) ? a.push(clusters_[i]) : j++;
    }
    clusters_ = a;

    
    // reset cache if clusters were destroyed
    if (tmpMarkers.length)
      clusterCache_ = [];
    setTimeout(function(){reAddMarkers_(tmpMarkers);}, 50);
  };


  /**
   * Add a set of markers.
   *
   * @param {Array of GMarker} markers The markers you want to add.
   */
  this.addMarkers = function (markers) {
    prepClusters(clusters_);
    setTimeout(function(){reAddMarkers_(markers);}, 50);    
  };

  // initialize
  if (typeof opt_markers === "object" && opt_markers !== null) {
    setTimeout(function(){me_.addMarkers(opt_markers);}, 50);
  }

  // on map move end, regroup
  mcfn_ = GEvent.addListener(map_, "moveend", function(){
    me_.resetViewport();
  });
}

/**
 * Create a cluster to collect markers.
 * A cluster includes some markers which are in a block of area.
 * If there are more than one markers in cluster, the cluster
 * will create a {@link ClusterMarker_} and show the total number
 * of markers in cluster.
 *
 * @constructor
 * @private
 * @param {HiveMapsClusterer} markerClusterer The marker cluster object
 */
function Cluster(markerClusterer, center) {
  var markerClusterer_ = markerClusterer;
  var map_ = markerClusterer.getMap_();
  var clusterMarker_ = null;

  this.markers = [];
  this.zoom = null;
  this.center = center;
  this.mapDivCenter = null;
  this.boundsLeft = null;
  this.boundsRight = null;
  this.boundsTop = null;
  this.boundsBottom = null;
  
  // initialize zoom, center, mapDivCenter, etc.
  markerClusterer_.prepClusters([this]);


  /**
   * If this cluster intersects certain bounds.
   *
   * @param {GLatLngBounds} bounds A bounds to test
   * @return {Boolean} Is this cluster intersects the bounds
   */
  this.isInBounds = function() {
    /* // Fastest method, but suffers from awkwardness when zoomed in and panning around.
       // Need to cache latlng bounds of marker according to zoom level
    var bounds = map_.getBounds();
    var sw = bounds.getSouthWest();
    var ne = bounds.getNorthEast(); 
    return (this.center.lat() > sw.lat() && this.center.lat() < ne.lat() &&
      this.center.lng() > sw.lng() && this.center.lng() < ne.lng());
    */
    
    var bounds = map_.getBounds();
    var sw = map_.fromLatLngToDivPixel(bounds.getSouthWest());
    var ne = map_.fromLatLngToDivPixel(bounds.getNorthEast());
    var centerxy = map_.fromLatLngToDivPixel(this.center);
    var gridSize = markerClusterer.getGridSize_();
    if (this.zoom !== map_.getZoom())
      gridSize = Math.pow(2, map_.getZoom()-this.zoom) * gridSize;
    
    if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x))
      return false;
    if (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)
      return false;
    return true;
  };

  /**
   * Add a marker.
   *
   * {GMarker} marker The marker you want to add.
   */
  this.addMarker = function (marker) {
    this.markers.push(marker);
  };

  /**
   * Remove a marker from cluster.
   *
   * @param {GMarker} marker The marker you want to remove.
   * @return {Boolean} Whether find the marker to be removed.
   */
  this.removeMarker = function (marker) {
    for (var i=this.markers.length; i--;) {
      if (marker === this.markers[i]) {
        if (this.markers[i].isOnMap) {
          map_.removeOverlay(this.markers[i]);
        }
        this.markers.splice(i, 1);
        return true;
      }
    }
    return false;
  };

  /**
   * Redraw a cluster.
   * @private
   * @param {Boolean} isForce If redraw by force, no matter if the cluster is
   *     in viewport.
   */
  this.redraw_ = function (isForce) {
    // protect against 0 size clusters
    if (!this.markers.length)
      return this.clearMarkers(true);
      
    if (!isForce && !this.isInBounds())
      return;
      
    var mz = markerClusterer.getMaxZoom_();
    if (mz === null)
      mz = map_.getCurrentMapType().getMaximumResolution();
      
    // Display unclustered markers if current zoom level is beyond the max zoom level
    // or the cluster has only one marker
    if (this.zoom >= mz || this.markers.length == 1) {
      var len = this.markers.length;
      for (var i=0; i<len; i++) {
        if (this.markers[i].isOnMap) {
          if (this.markers[i].isHidden()) {
            this.markers[i].show();
          }
        } else {
          map_.addOverlay(this.markers[i]);
          this.markers[i].isOnMap = true;
        }
      }
      if (clusterMarker_ !== null)
        clusterMarker_.hide();
      
    // Add a cluster marker on map to show the number of markers in this cluster.
    } else {  
      // hide all markers in the cluster if needed
      //if (this.markers.length && this.markers[0].isOnMap && !this.markers[0].isHidden()) {
      var len = this.markers.length;
      for (var i=0; i<len; i++) {
        if (this.markers[i].isOnMap && !this.markers[i].isHidden())
          this.markers[i].hide();
      }
      if (clusterMarker_ === null) {
        clusterMarker_ = new ClusterMarker_(this.center, this.getTotalMarkers(), markerClusterer_.getStyles_(), markerClusterer_.getGridSize_());
        map_.addOverlay(clusterMarker_);
      } else {
        if (clusterMarker_.isHidden())
          clusterMarker_.show();
        clusterMarker_.redraw(true);
      }
    }
  };

  /**
   * Remove all the markers from this cluster.
   */
  this.clearMarkers = function (leaveOnMap) {
    if (clusterMarker_ !== null) {
      map_.removeOverlay(clusterMarker_);
    }
    if (!leaveOnMap) {
      for (var i=this.markers.length; i--;) {
        if (this.markers[i].isOnMap) {
          map_.removeOverlay(this.markers[i]);
        }
      }
    }
    this.markers = [];
  };

  /**
   * Get number of markers.
   * @return {Number}
   */
  this.getTotalMarkers = function () {
    return this.markers.length;
  };
}

/**
 * ClusterMarker_ creates a marker that shows the number of markers that
 * a cluster contains.
 *
 * @constructor
 * @private
 * @param {GLatLng} latlng Marker's lat and lng.
 * @param {Number} count Number to show.
 * @param {Array of Object} styles The image list to be showed:
 *   {String} url Image url.
 *   {Number} height Image height.
 *   {Number} width Image width.
 *   {Array of Number} anchor Text anchor of image left and top.
 *   {String} textColor text color.
 * @param {Number} padding Padding of marker center.
 */
function ClusterMarker_(latlng, count, styles, padding) {
  var index = 0;
  var dv = count;
  while (dv !== 0) {
    dv = parseInt(dv / 10, 10);
    index++;
  }
  // todo: figure out why index does not get set properly sometimes
  if (index == 0) {
    index = 1;
  } else if (styles.length < index) {
    index = styles.length;
  }
  this.url_ = styles[index - 1].url;
  this.height_ = styles[index - 1].height;
  this.width_ = styles[index - 1].width;
  this.textColor_ = styles[index - 1].opt_textColor;
  this.anchor_ = styles[index - 1].opt_anchor;
  this.latlng_ = latlng;
  this.index_ = index;
  this.styles_ = styles;
  this.text_ = count;
  this.padding_ = padding;
}

ClusterMarker_.prototype = new GOverlay();

/**
 * Initialize cluster marker.
 * @private
 */
ClusterMarker_.prototype.initialize = function (map) {
  this.map_ = map;
  var div = document.createElement("div");
  var latlng = this.latlng_;
  var pos = map.fromLatLngToDivPixel(latlng);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  var mstyle = "";
  if (document.all) {
    mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");';
  } else {
    mstyle = "background:url(" + this.url_ + ");";
  }
  if (typeof this.anchor_ === "object") {
    if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) {
      mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;';
    } else {
      mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
    }
    if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) {
      mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;';
    } else {
      mstyle += 'width:' + this.width_ + 'px;text-align:center;';
    }
  } else {
    mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
    mstyle += 'width:' + this.width_ + 'px;text-align:center;';
  }
  var txtColor = this.textColor_ ? this.textColor_ : 'black';

  div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" +
      pos.x + "px;color:" + txtColor +  ";position:absolute;font-size:11px;" +
      'font-family:Arial,sans-serif;font-weight:bold';
  div.innerHTML = this.text_;
  map.getPane(G_MAP_MAP_PANE).appendChild(div);
  var padding = this.padding_;
  GEvent.addDomListener(div, "click", function () {
    var pos = map.fromLatLngToDivPixel(latlng);
    var sw = new GPoint(pos.x - padding, pos.y + padding);
    sw = map.fromDivPixelToLatLng(sw);
    var ne = new GPoint(pos.x + padding, pos.y - padding);
    ne = map.fromDivPixelToLatLng(ne);
    var zoom = map.getBoundsZoomLevel(new GLatLngBounds(sw, ne), map.getSize());
    map.setCenter(latlng, zoom);
  });
  this.div_ = div;
};

/**
 * Remove this overlay.
 * @private
 */
ClusterMarker_.prototype.remove = function () {
  this.div_.parentNode.removeChild(this.div_);
};

/**
 * Copy this overlay.
 * @private
 */
ClusterMarker_.prototype.copy = function () {
  return new ClusterMarker_(this.latlng_, this.index_, this.text_, this.styles_, this.padding_);
};

/**
 * Redraw this overlay.
 * @private
 */
ClusterMarker_.prototype.redraw = function (force) {
  if (!force) {
    return;
  }
  var pos = this.map_.fromLatLngToDivPixel(this.latlng_);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  this.div_.style.top =  pos.y + "px";
  this.div_.style.left = pos.x + "px";
};

/**
 * Hide this cluster marker.
 */
ClusterMarker_.prototype.hide = function () {
  this.div_.style.display = "none";
};

/**
 * Show this cluster marker.
 */
ClusterMarker_.prototype.show = function () {
  this.div_.style.display = "";
};

/**
 * Get whether the cluster marker is hidden.
 * @return {Boolean}
 */
ClusterMarker_.prototype.isHidden = function () {
  return this.div_.style.display === "none";
};
